/*
 * Copyright 2012 Netflix, Inc.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.netflix.eureka;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Date;

import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.CloudInstanceConfig;
import com.netflix.appinfo.DataCenterInfo;
import com.netflix.appinfo.EurekaInstanceConfig;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.MyDataCenterInstanceConfig;
import com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DeploymentContext;
import com.netflix.discovery.DefaultEurekaClientConfig;
import com.netflix.discovery.DiscoveryClient;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.EurekaClientConfig;
import com.netflix.discovery.converters.JsonXStream;
import com.netflix.discovery.converters.XmlXStream;
import com.netflix.eureka.aws.AwsBinder;
import com.netflix.eureka.aws.AwsBinderDelegate;
import com.netflix.eureka.cluster.PeerEurekaNodes;
import com.netflix.eureka.registry.AwsInstanceRegistry;
import com.netflix.eureka.registry.PeerAwareInstanceRegistry;
import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl;
import com.netflix.eureka.resources.DefaultServerCodecs;
import com.netflix.eureka.resources.ServerCodecs;
import com.netflix.eureka.util.EurekaMonitors;
import com.thoughtworks.xstream.XStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The class that kick starts the eureka server.
 *
 * <p>
 * The eureka server is configured by using the configuration
 * {@link EurekaServerConfig} specified by <em>eureka.server.props</em> in the
 * classpath.  The eureka client component is also initialized by using the
 * configuration {@link EurekaInstanceConfig} specified by
 * <em>eureka.client.props</em>. If the server runs in the AWS cloud, the eureka
 * server binds it to the elastic ip as specified.
 * </p>
 *
 * @author Karthik Ranganathan, Greg Kim, David Liu
 *
 */
public class EurekaBootStrap implements ServletContextListener {
    private static final Logger logger = LoggerFactory.getLogger(EurekaBootStrap.class);

    private static final String TEST = "test";

    private static final String ARCHAIUS_DEPLOYMENT_ENVIRONMENT = "archaius.deployment.environment";

    private static final String EUREKA_ENVIRONMENT = "eureka.environment";
    /**
     * 数据部署中心-CLOUD
     */
    private static final String CLOUD = "cloud";
    /**
     * 数据部署中心-默认
     */
    private static final String DEFAULT = "default";

    private static final String ARCHAIUS_DEPLOYMENT_DATACENTER = "archaius.deployment.datacenter";

    private static final String EUREKA_DATACENTER = "eureka.datacenter";

    protected volatile EurekaServerContext serverContext;
    protected volatile AwsBinder awsBinder;
    
    private EurekaClient eurekaClient;

    /**
     * Construct a default instance of Eureka boostrap
     */
    public EurekaBootStrap() {
        this(null);
    }
    
    /**
     * Construct an instance of eureka bootstrap with the supplied eureka client
     * 
     * @param eurekaClient the eureka client to bootstrap
     */
    public EurekaBootStrap(EurekaClient eurekaClient) {
        this.eurekaClient = eurekaClient;
    }

    /**
     *
     *  初始化eureka 服务，并启动eureka 服务。eureka 服务启动的入口
     *
     *  由于eureka server 服务需要借助 web容器 启动服务。所以eureka server 实现了ServletContextListener 接口，
     *
     *  当Servlet 容器启动或终止Web 应用时，会触发ServletContextEvent 事件，该事件由ServletContextListener 来处理。
     *  当Servlet 容器启动Web 应用时调用contextInitialized方法。完成eureka 服务的初始化工作及启动eureka 服务。
     *
     *  Initializes Eureka, including syncing up with other Eureka peers and publishing the registry.
     *
     * @see
     * javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        try {

            /**
             * 初始化eureka 的运行环境。 是否运行去上面，使用Environment配置我们是在测试环境、生产环境、开发环境
             * 1、dataCenter另一个可配置的值是cloud，表示使用AWS
             * 2、设置Environment，默认情况下是test。可以在配置文件中通过”eureka.environment”修改。在测试环境、生产环境、开发环境
             */
            initEurekaEnvironment();

            /**
             * 初始化Eureka-Server上下文
              */
            initEurekaServerContext();

            ServletContext sc = event.getServletContext();
            sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
        } catch (Throwable e) {
            logger.error("Cannot bootstrap eureka server :", e);
            throw new RuntimeException("Cannot bootstrap eureka server :", e);
        }
    }

    /**
     *  初始化eureka server 的配置环境
     * Users can override to initialize the environment themselves.
     */
    protected void initEurekaEnvironment() throws Exception {
        logger.info("Setting the eureka configuration..");

        //1、设置配置文件的数据中心 -Deureka.datacenter=cloud，这样eureka将会知道 eureka 服务是运行在AWS云上。
        String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
        if (dataCenter == null) {
            logger.info("Eureka data center value eureka.datacenter is not set, defaulting to default");
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT);
        }
        else {
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter);
        }


        //2、设置配置文件的环境，默认为test Environment配置我们是在测试环境、生产环境、开发环境等
        String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
        if (environment == null) {
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
            logger.info("Eureka environment value eureka.environment is not set, defaulting to test");
        }
    }

    /**
     *
     *  初始化eureka server 上下文。
     * init hook for server context. Override for custom logic.
     */
    protected void initEurekaServerContext() throws Exception {
        /**
         *
         *  第一步：加载eureka-server.properties 配置文件里的配置信息。
         *  创建DefaultEurekaServerConfig 对象，并加载eureka-server.properties中的配置
         *  该配置都是Server需要的信息，在DefaultEurekaServerConfig中都提供了默认的配置, 可以加载”eureka-server.properties”配置文件覆盖默认的配置
         */
        EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();

        // For backward compatibility
        JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
        XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);

        logger.info("Initializing the eureka client...");
        logger.info(eurekaServerConfig.getJsonCodecName());

        //创建 Eureka-Server 请求和响应编解码器
        ServerCodecs serverCodecs = new DefaultServerCodecs(eurekaServerConfig);

        /**
         *  初始化eureka server 内部的一个eureka-client
         *  作用：用来跟其它的eureka-server 节点进行注册和通信的。
         *  在EurekaInstanceConfig的基础上，能够提供初始化注册到Server所需要的信息，并且能够被其他模块获取。
         *
         *  ApplicationInfoManager（包含了服务实例的信息、配置，作为服务实例管理的一个组件）
         *
         *
         */
        ApplicationInfoManager applicationInfoManager = null;

        /**创建 Eureka-Client 实例
         * 是Server的使用者，provider和consumer都可以称作是Eureka Client
          */
        if (eurekaClient == null) {

            /**
             *   EurekaInstanceConfig中，运行的当前eureka server 这个服务, 可以看成当前运行服务的实例
             *
             *   其实就是将eureka-client.properties文件中的配置加载到ConfigurationManager中去，
             *   然后基于EurekaInstanceConfig对外暴露的接口来获取这个eureka-client.properties文件中的一些配置项的读取，
             *   而且人家提供了所有配置项的默认值
             *
             *   EurekaInstanceConfig是服务实例相关的一些配置。eureka server同时也是一个eureka client，
             *   因为他可能要向其他的eureka server去进行注册，组成一个eureka server的集群。
             *   eureka server把自己也当做是一个eureka client，也就是一个服务实例，
             *   所以他这里肯定也是有所谓的Application、Instance等概念的
             */
            EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
                    ? new CloudInstanceConfig()
                    : new MyDataCenterInstanceConfig();

            /**
             *  get() 返回InstanceInfo 实例
             * InstanceInfo， 运行的当前eureka server 这个服务实例的一些配置信息
             * 读取各种各样的服务实例相关的配置信息，再构造了几个其他的对象，最终完成了InstanceInfo的构建
             */
            applicationInfoManager = new ApplicationInfoManager(instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());

            /**
             *  EurekaClientConfig 接口代表了，eureka client 的配置信息。
             *  EurekaClient相关的一些配置项。也是去读eureka-client.properties里的一些配置
             *
             * eureka server自己本身代表的一个服务实例，把自己作为一个服务注册到别的eureka server上去，
             */
            EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();

            /**
             *  创建eureka client 对象，
             *  在创建eureka client 的过程中，从eureka server 拉取全量的注册表信息，缓存在本地。
             * eurekaClient 包括服务提供者和消费，也包括eureka server 本身。
             */
            eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
        } else {
            applicationInfoManager = eurekaClient.getApplicationInfoManager();
        }

        /**
         * 处理注册相关的事情
         * peers集群，peer就是集群中的一个实例
         * InstanceRegistry：实例注册，服务实例注册，注册表，这个里面放了所有的注册到这个eureka server上来的服务实例，就是一个服务实例的注册表
         *
         * PeerAwareInstanceRegistry ： eureka server集群的服务实例的注册表。即注册的每一个eureka client 信息。
         *
         */
        PeerAwareInstanceRegistry registry;
        if (isAws(applicationInfoManager.getInfo())) {//AWS相关，跳过
            registry = new AwsInstanceRegistry(
                    eurekaServerConfig,
                    eurekaClient.getEurekaClientConfig(),
                    serverCodecs,
                    eurekaClient
            );
            awsBinder = new AwsBinderDelegate(eurekaServerConfig, eurekaClient.getEurekaClientConfig(), registry, applicationInfoManager);
            awsBinder.start();
        }
        else {
            registry = new PeerAwareInstanceRegistryImpl(
                    eurekaServerConfig,
                    eurekaClient.getEurekaClientConfig(),
                    serverCodecs,
                    eurekaClient
            );
        }


        /**
         * 第四步，PeerEurekaNodes 代表了eureka server 注册中心 集群
         * 处理peer 节点之间的通信
         * peers大概来说多个相同的实例组成的一个集群，peer就是peers集群中的一个实例
         */
        PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
                registry,
                eurekaServerConfig,
                eurekaClient.getEurekaClientConfig(),
                serverCodecs,
                applicationInfoManager
        );

        /**
         * 创建 EurekaServer 上下文(context)
         * EurekaServerContext，代表了当前这个eureka server的一个服务器上下文，包含了服务器需要的所有的信秘。
         * 将这个对象放在了一个holder中，以后谁如果要使用这个EurekaServerContext，直接从这个holder中获取就可以了
         */
        serverContext = new DefaultEurekaServerContext(
                eurekaServerConfig,
                serverCodecs,
                registry,
                peerEurekaNodes,
                applicationInfoManager
        );

        // 初始化EurekaServerContext 上下文
        //Eureka-Server 上下文持有者。通过它，可以很方便的获取到 Eureka-Server 上下文
        EurekaServerContextHolder.initialize(serverContext);

        serverContext.initialize();
        logger.info("Initialized server context");
        // 从其他Eureka-Server拉取注册信息
        //从相邻的一个eureka server节点拷贝注册表的信息，如果拷贝失败，就找下一个
        int registryCount = registry.syncUp();
        // 注册表中服务实例自动感知，服务是否下线，是否发生故障
        registry.openForTraffic(applicationInfoManager, registryCount);

        // Register all monitoring statistics.
        //注册所有监视器的统计。
        //eureka自身的监控机制相关联的
        EurekaMonitors.registerAllStats();
    }
    
    protected PeerEurekaNodes getPeerEurekaNodes(PeerAwareInstanceRegistry registry, EurekaServerConfig eurekaServerConfig, EurekaClientConfig eurekaClientConfig, ServerCodecs serverCodecs, ApplicationInfoManager applicationInfoManager) {
        PeerEurekaNodes peerEurekaNodes = new PeerEurekaNodes(
                registry,
                eurekaServerConfig,
                eurekaClientConfig,
                serverCodecs,
                applicationInfoManager
        );
        
        return peerEurekaNodes;
    }

    /**
     * Handles Eureka cleanup, including shutting down all monitors and yielding all EIPs.
     *
     * @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
     */
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        try {
            logger.info("{} Shutting down Eureka Server..", new Date().toString());
            ServletContext sc = event.getServletContext();
            sc.removeAttribute(EurekaServerContext.class.getName());

            destroyEurekaServerContext();
            destroyEurekaEnvironment();

        } catch (Throwable e) {
            logger.error("Error shutting down eureka", e);
        }
        logger.info("{} Eureka Service is now shutdown...", new Date().toString());
    }

    /**
     * Server context shutdown hook. Override for custom logic
     */
    protected void destroyEurekaServerContext() throws Exception {
        EurekaMonitors.shutdown();
        if (awsBinder != null) {
            awsBinder.shutdown();
        }
        if (serverContext != null) {
            serverContext.shutdown();
        }
    }

    /**
     * Users can override to clean up the environment themselves.
     */
    protected void destroyEurekaEnvironment() throws Exception {

    }

    protected boolean isAws(InstanceInfo selfInstanceInfo) {
        boolean result = DataCenterInfo.Name.Amazon == selfInstanceInfo.getDataCenterInfo().getName();
        logger.info("isAws returned {}", result);
        return result;
    }

    protected boolean isCloud(DeploymentContext deploymentContext) {
        logger.info("Deployment datacenter is {}", deploymentContext.getDeploymentDatacenter());
        return CLOUD.equals(deploymentContext.getDeploymentDatacenter());
    }
}
